#!/usr/bin/python
# _*_ coding: utf-8 _*_

"""

@author: Qing Liu, sunnymarkliu@163.com
@github: https://github.com/sunnymarkLiu
@time  : 2019/5/9 16:00
"""
from __future__ import absolute_import, division, print_function

import os
import sys

module_path = os.path.abspath(os.path.join('..'))
sys.path.append(module_path)
from fuzzywuzzy import fuzz
import math
import numpy as np
from nltk.corpus import brown
from nltk.corpus import wordnet as wn
from collections import Counter
from scipy import spatial
import warnings
warnings.filterwarnings('ignore')


class DistanceUtil(object):

    @staticmethod
    def jaccard_similarity_distance(set_a, set_b):
        """
        Jaccard Similarity Distance
        """
        intersection = set(set_a).intersection(set(set_b))
        union = set(set_a).union(set(set_b))
        if len(union) == 0:
            return 0.0
        return 1.0 * len(intersection) / len(union)

    @staticmethod
    def countbased_cos_distance(tokenization1, tokenization2):
        """
        基于计数的 cos 距离
        """
        def build_vector(iterable1, iterable2):
            counter1 = Counter(iterable1)
            counter2 = Counter(iterable2)
            all_items = set(counter1.keys()).union(set(counter2.keys()))
            vector1 = [counter1[k] for k in all_items]
            vector2 = [counter2[k] for k in all_items]
            vector1 = [1e-6] if len(vector1) == 0 else vector1
            vector2 = [1e-6] if len(vector2) == 0 else vector2
            return vector1, vector2

        v1, v2 = build_vector(tokenization1, tokenization2)
        dist = 1 - spatial.distance.cosine(v1, v2)
        return dist

    @staticmethod
    def levenshtein_distance(s1, s2):
        """
        莱文斯坦距离，又称Levenshtein距离/ edit distance，是编辑距离的一种。指两个字串之間，由一个转成另一个所需的最少编辑操作次数。
        允许的编辑操作包括将一个字符替换成另一个字符，插入一个字符，刪除一个字符。
        """
        if len(s1) > len(s2):
            s1, s2 = s2, s1

        distances = range(len(s1) + 1)
        for i2, c2 in enumerate(s2):
            distances_ = [i2 + 1]
            for i1, c1 in enumerate(s1):
                if c1 == c2:
                    distances_.append(distances[i1])
                else:
                    distances_.append(1 + min((distances[i1], distances[i1 + 1], distances_[-1])))
            distances = distances_
        return distances[-1]

    @staticmethod
    def fuzzy_matching_ratio(str1, str2, ratio_func='partial_ratio'):
        """
        字符串模糊匹配
        :param str1: 字符串
        :param str2: 字符串
        :param ratio_func: ratio, partial_ratio, token_sort_ratio, token_set_ratio
        :return:
        """
        if ratio_func == 'ratio':
            # Normalize to [0 - 1] range.
            return fuzz.ratio(str1, str2) / 100.0
        if ratio_func == 'partial_ratio':
            return fuzz.partial_ratio(str1, str2) / 100.0
        if ratio_func == 'token_sort_ratio':
            return fuzz.token_sort_ratio(str1, str2) / 100.0
        if ratio_func == 'token_set_ratio':
            return fuzz.token_set_ratio(str1, str2) / 100.0

    @staticmethod
    def wordnet_path_distances(sentence1, sentence2, info_content_norm):
        """
        wordnet path distances
        """
        # Parameters to the algorithm. Currently set to values that was reported
        # in the paper to produce "best" results.
        ALPHA = 0.2
        BETA = 0.45
        ETA = 0.4
        PHI = 0.2
        DELTA = 0.85
        brown_freqs = dict()

        ######################### word similarity ##########################

        def get_best_synset_pair(word_1, word_2):
            """
            Choose the pair with highest path similarity among all pairs.
            Mimics pattern-seeking behavior of humans.
            """
            max_sim = -1.0
            synsets_1 = wn.synsets(word_1)
            synsets_2 = wn.synsets(word_2)
            if len(synsets_1) == 0 or len(synsets_2) == 0:
                return None, None
            else:
                max_sim = -1.0
                best_pair = None, None
                for synset_1 in synsets_1:
                    for synset_2 in synsets_2:
                        sim = wn.path_similarity(synset_1, synset_2)
                        if sim is not None and sim > max_sim:
                            max_sim = sim
                            best_pair = synset_1, synset_2
                return best_pair

        def length_dist(synset_1, synset_2):
            """
            Return a measure of the length of the shortest path in the semantic
            ontology (Wordnet in our case as well as the paper's) between two
            synsets.
            """
            l_dist = 1e9
            if synset_1 is None or synset_2 is None:
                return 0.0
            if synset_1 == synset_2:
                # if synset_1 and synset_2 are the same synset return 0
                l_dist = 0.0
            else:
                wset_1 = set([str(x.name()) for x in synset_1.lemmas()])
                wset_2 = set([str(x.name()) for x in synset_2.lemmas()])
                if len(wset_1.intersection(wset_2)) > 0:
                    # if synset_1 != synset_2 but there is word overlap, return 1.0
                    l_dist = 1.0
                else:
                    # just compute the shortest path between the two
                    l_dist = synset_1.shortest_path_distance(synset_2)
                    if l_dist is None:
                        l_dist = 0.0
            # normalize path length to the range [0,1]
            return math.exp(-ALPHA * l_dist)

        def hierarchy_dist(synset_1, synset_2):
            """
            Return a measure of depth in the ontology to model the fact that
            nodes closer to the root are broader and have less semantic similarity
            than nodes further away from the root.
            """
            h_dist = 1e9
            if synset_1 is None or synset_2 is None:
                return h_dist
            if synset_1 == synset_2:
                # return the depth of one of synset_1 or synset_2
                h_dist = max([x[1] for x in synset_1.hypernym_distances()])
            else:
                # find the max depth of least common subsumer
                hypernyms_1 = {x[0]: x[1] for x in synset_1.hypernym_distances()}
                hypernyms_2 = {x[0]: x[1] for x in synset_2.hypernym_distances()}
                lcs_candidates = set(hypernyms_1.keys()).intersection(
                    set(hypernyms_2.keys()))
                if len(lcs_candidates) > 0:
                    lcs_dists = []
                    for lcs_candidate in lcs_candidates:
                        lcs_d1 = 0
                        if lcs_candidate in hypernyms_1:
                            lcs_d1 = hypernyms_1[lcs_candidate]
                        lcs_d2 = 0
                        if lcs_candidate in hypernyms_2:
                            lcs_d2 = hypernyms_2[lcs_candidate]
                        lcs_dists.append(max([lcs_d1, lcs_d2]))
                    h_dist = max(lcs_dists)
                else:
                    h_dist = 0
            return ((math.exp(BETA * h_dist) - math.exp(-BETA * h_dist)) /
                    (math.exp(BETA * h_dist) + math.exp(-BETA * h_dist)))

        def word_similarity(word_1, word_2):
            synset_pair = get_best_synset_pair(word_1, word_2)
            return (length_dist(synset_pair[0], synset_pair[1]) *
                    hierarchy_dist(synset_pair[0], synset_pair[1]))

        ######################### sentence similarity ##########################

        def most_similar_word(word, word_set):
            """
            Find the word in the joint word set that is most similar to the word
            passed in. We use the algorithm above to compute word similarity between
            the word and each word in the joint word set, and return the most similar
            word and the actual similarity value.
            """
            max_sim = -1.0
            sim_word = ""
            for ref_word in word_set:
                sim = word_similarity(word, ref_word)
                if sim > max_sim:
                    max_sim = sim
                    sim_word = ref_word
            return sim_word, max_sim

        def info_content(lookup_word):
            """
            Uses the Brown corpus available in NLTK to calculate a Laplace
            smoothed frequency distribution of words, then uses this information
            to compute the information content of the lookup_word.
            """
            global N
            if N == 0:
                # poor man's lazy evaluation
                for sent in brown.sents():
                    for word in sent:
                        word = word.lower()
                        if word not in brown_freqs:
                            brown_freqs[word] = 0
                        brown_freqs[word] = brown_freqs[word] + 1
                        N = N + 1
            lookup_word = lookup_word.lower()
            n = 0 if lookup_word not in brown_freqs else brown_freqs[lookup_word]
            return 1.0 - (math.log(n + 1) / math.log(N + 1))

        def semantic_vector(words, joint_words, info_content_norm):
            """
            Computes the semantic vector of a sentence. The sentence is passed in as
            a collection of words. The size of the semantic vector is the same as the
            size of the joint word set. The elements are 1 if a word in the sentence
            already exists in the joint word set, or the similarity of the word to the
            most similar word in the joint word set if it doesn't. Both values are
            further normalized by the word's (and similar word's) information content
            if info_content_norm is True.
            """
            sent_set = set(words)
            semvec = np.zeros(len(joint_words))
            i = 0
            for joint_word in joint_words:
                if joint_word in sent_set:
                    # if word in union exists in the sentence, s(i) = 1 (unnormalized)
                    semvec[i] = 1.0
                    if info_content_norm:
                        semvec[i] = semvec[i] * math.pow(info_content(joint_word), 2)
                else:
                    # find the most similar word in the joint set and set the sim value
                    sim_word, max_sim = most_similar_word(joint_word, sent_set)
                    semvec[i] = PHI if max_sim > PHI else 0.0
                    if info_content_norm:
                        semvec[i] = semvec[i] * info_content(joint_word) * info_content(sim_word)
                i = i + 1
            return semvec

        def semantic_similarity(words_1, words_2, info_content_norm):
            """
            Computes the semantic similarity between two sentences as the cosine
            similarity between the semantic vectors computed for each sentence.
            """
            joint_words = set(words_1).union(set(words_2))
            vec_1 = semantic_vector(words_1, joint_words, info_content_norm)
            vec_2 = semantic_vector(words_2, joint_words, info_content_norm)
            return spatial.distance.cosine(vec_1, vec_2)

        ######################### word order similarity ##########################

        def word_order_vector(words, joint_words, windex):
            """
            Computes the word order vector for a sentence. The sentence is passed
            in as a collection of words. The size of the word order vector is the
            same as the size of the joint word set. The elements of the word order
            vector are the position mapping (from the windex dictionary) of the
            word in the joint set if the word exists in the sentence. If the word
            does not exist in the sentence, then the value of the element is the
            position of the most similar word in the sentence as long as the similarity
            is above the threshold ETA.
            """
            wovec = np.zeros(len(joint_words))
            i = 0
            wordset = set(words)
            for joint_word in joint_words:
                if joint_word in wordset:
                    # word in joint_words found in sentence, just populate the index
                    wovec[i] = windex[joint_word]
                else:
                    # word not in joint_words, find most similar word and populate
                    # word_vector with the thresholded similarity
                    sim_word, max_sim = most_similar_word(joint_word, wordset)
                    if max_sim > ETA:
                        wovec[i] = windex[sim_word]
                    else:
                        wovec[i] = 0
                i = i + 1
            return wovec

        def word_order_similarity(words_1, words_2):
            """
            Computes the word-order similarity between two sentences as the normalized
            difference of word order between the two sentences.
            """
            joint_words = list(set(words_1).union(set(words_2)))
            windex = {x[1]: x[0] for x in enumerate(joint_words)}
            r1 = word_order_vector(words_1, joint_words, windex)
            r2 = word_order_vector(words_2, joint_words, windex)
            return 1.0 - (np.linalg.norm(r1 - r2) / np.linalg.norm(r1 + r2))

        ######################### overall similarity ##########################

        def similarity(words_1, words_2, content_norm):
            """
            Calculate the semantic similarity between two sentences. The last
            parameter is True or False depending on whether information content
            normalization is desired or not.
            """
            return DELTA * semantic_similarity(words_1, words_2, content_norm) + \
                   (1.0 - DELTA) * word_order_similarity(words_1, words_2)

        return similarity(sentence1, sentence2, info_content_norm)
